# -*- coding: utf-8 -*-
# @Time    : 2024/7/2 15:03
# @Author  : yujiahao
# @File    : 37_async_io.py
# @description:异步io


"""

爬虫是典型的 I/O 密集型任务，I/O 密集型任务的特点就是程序会经常性的因为 I/O 操作而进入阻塞状态，比如使用requests获取页面代码或二进制内容，
发出一个请求之后，程序必须要等待网站返回响应之后才能继续运行，如果目标网站不是很给力或者网络状况不是很理想，那么等待响应的时间可能会很久，而在这个过程
中整个程序是一直阻塞在那里，没有做任何的事情。通过前面的课程，我们已经知道了可以通过多线程的方式为爬虫提速，使用多线程的本质就是，当一个线程阻塞的时候，
程序还有其他的线程可以继续运转，因此整个程序就不会在阻塞和等待中浪费了大量的时间。

事实上，还有一种非常适合 I/O 密集型任务的并发编程方式，称之为异步编程，你也可以将它称为异步 I/O。这种方式并不需要启动多个线程或多个进程来实现并发，
它是通过多个子程序相互协作的方式来提升 CPU 的利用率，解决了 I/O 密集型任务 CPU 利用率很低的问题，一般将这种方式称为“协作式并发”。


【基本概念】

【阻塞】
    阻塞状态指程序未得到所需计算资源时被挂起的状态。程序在等待某个操作完成期间，自身无法继续处理其他的事情，则称该程序在该操作上是阻塞的。
    阻塞随时都可能发生，最典型的就是 I/O 中断（包括网络 I/O 、磁盘 I/O 、用户输入等）、休眠操作、等待某个线程执行结束，甚至包括在 CPU
    切换上下文时，程序都无法真正的执行，这就是所谓的阻塞。

【非阻塞】
    程序在等待某操作过程中，自身不被阻塞，可以继续处理其他的事情，则称该程序在该操作上是非阻塞的。非阻塞并不是在任何程序级别、任何情况下都可以存在的。
    仅当程序封装的级别可以囊括独立的子程序单元时，它才可能存在非阻塞状态。显然，某个操作的阻塞可能会导程序耗时以及效率低下，所以我们会希望把它变成非阻塞的。

【同步】
    不同程序单元为了完成某个任务，在执行过程中需靠某种通信方式以协调一致，我们称这些程序单元是同步执行的。例如前面讲过的给银行账户存钱的操作，
    我们在代码中使用了“锁”作为通信信号，让多个存钱操作强制排队顺序执行，这就是所谓的同步。

【异步】
    不同程序单元在执行过程中无需通信协调，也能够完成一个任务，这种方式我们就称之为异步。例如，使用爬虫下载页面时，调度程序调用下载程序后，
    即可调度其他任务，而无需与该下载任务保持通信以协调行为。不同网页的下载、保存等操作都是不相关的，也无需相互通知协调。很显然，异步操作
    的完成时刻和先后顺序并不能确定。

同步与异步的关注点是消息通信机制，最终表现出来的是“有序”和“无序”的区别；阻塞和非阻塞的关注点是程序在等待消息时状态，
最终表现出来的是程序在等待时能不能做点别的。如果想深入理解这些内容，推荐大家阅读经典著作《UNIX网络编程》，这本书非常的赞。

"""
import time
import asyncio

# 生成器和协程

'''
异步编程是一种“协作式并发”，即通过多个子程序相互协作的方式提升 CPU 的利用率，从而减少程序在阻塞和等待中浪费的时间，最终达到并发的效果。
我们可以将多个相互协作的子程序称为“协程”，它是实现异步编程的关键。在介绍协程之前，我们先通过下面的代码，复习一下生成器。
'''


# 编写了一个生成斐波那契数列的生成器

def fib(max_count):
    a, b = 0, 1
    for _ in range(max_count):
        a, b = b, a + b
        yield a


# 生成器经过预激活，就是一个协程，它可以跟其他子程序协作。
def calc_average():
    total, counter = 0, 0
    avg_value = None
    while True:
        # 这里yield会吧send接受到的参数赋值给左边，把右边的值返回回去
        curr_value = yield avg_value
        total += curr_value
        counter += 1
        avg_value = total / counter


print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')


# 下面看一个例子：

# 同步
def display(num):
    time.sleep(1)
    print(num)


# 异步
async def display2(num):
    await asyncio.sleep(1)
    print(num)


def use_display():
    start = time.time()
    for i in range(1, 10):
        display(i)
    end = time.time()
    print(f'{end - start:.3f}秒')


# 使用异步的方式改写上面的代码，让display函数以异步的方式运转。

def use_display2():
    # 记录开始时间
    start = time.time()

    # 创建协程对象列表，每个协程对象对应 display2 函数
    objs = [display2(i) for i in range(1, 10)]

    # 创建一个新的事件循环
    loop = asyncio.new_event_loop()
    # 设置这个新的事件循环为当前默认的事件循环
    asyncio.set_event_loop(loop)

    # 将协程对象显式地转换为任务对象
    tasks = [loop.create_task(obj) for obj in objs]

    # 运行任务，直到所有任务完成
    loop.run_until_complete(asyncio.wait(tasks))
    # 关闭事件循环
    loop.close()

    # 记录结束时间
    end = time.time()
    # 打印运行时间
    print(f'{end - start:.3f}秒')


def main():
    # 普通方法
    # gen_obj = fib(20)
    # for value in gen_obj:
    #     print(value)

    # 生成器
    '''
    main函数首先通过生成器对象的send方法发送一个None值来将其激活为协程，也可以通过next(obj)达到同样的效果。
    接下来，协程对象会接收main函数发送的数据并产出（yield）数据的平均值。
    '''

    obj = calc_average()
    # 生成器预激活
    obj.send(None)
    for _ in range(0):
        print(obj.send(float(input("请输入一个数字>> "))))
    print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

    # 每次执行都会依次输出1到9的数字，每个间隔1秒钟，整个代码需要执行大概需要9秒多的时间。，这段代码就是以同步和阻塞的方式执行的，
    # 同步可以从代码的输出看出来，而阻塞是指在调用display函数发生休眠时，整个代码的其他部分都不能继续执行，必须等待休眠结束。
    # use_display()

    # 使用异步的方式改写上面的代码，让display函数以异步的方式运转。
    use_display2()


if __name__ == '__main__':
    main()
